Log In  
[back to top]

[ :: Read More :: ]

Cart #mine1k-0 | 2022-10-09 | Code ▽ | Embed ▽ | No License
20

A demake of the classic minesweeper.

The game cartridge is just 1024 bytes -- see https://gist.github.com/pancelor/a3aadc5e8cdf809cf0a4972ac9598433 for some lightly commented source code

RULES / CONTROLS:

  • left click to reveal a tile
    • if you hit a mine, you lose
    • revealed tiles will show a number, telling how many of their 8 neighbors are mines
  • right click to flag a tile
  • reveal all non-mine tiles to win!
  • click the smiley face to restart

TIPS

  • left click + right click (simultaneous) to auto-reveal neighbors, if the number of nearby flags matches the number on the tile you clicked
  • mines left and a timer are displayed in the top corners
P#118854 2022-10-09 21:57

[ :: Read More :: ]

When exporting a game to a binary format (.exe, etc), the manual says:

> To include an extra file in the output folders and archives, use the -E switch:

> > EXPORT -E README.TXT MYGAME.BIN

I tried this (pico8 game.p8 -export "-f game.bin -e examples/ -e samples/") but it doesn't include those subfolders. If I -e examples/kick.pcm, then that file is included, but it's included at the top level, and not in an "examples" subfolder

Am I doing this wrong somehow? I assume this just isn't supported (yet? fingers crossed)

P#111906 2022-05-16 22:38 ( Edited 2022-06-04 19:26)

[ :: Read More :: ]

Cart #imhungry-0 | 2022-04-26 | Code ▽ | Embed ▽ | License: CC4-BY-NC-SA
2

> Vous contrôlez un petit rennes qui doit attraper le plus de nourriture possible, il s'agit de pain et de mûres. Plus vous attrapez de, plus la nourriture va vite et plus il est compliqué de l'attraper. En haut à droite, vous verrez qu'il y a votre tableau de bord, chaque aliment pêché vaut un point. Et en haut à gauche, il y a un panneau qui vous montre combien d'aliments vous n'avez pas attrapés. Attention ! au bout d'une dizaine d'aliments non attrapés vous perdez et le jeu affiche alors « GAME OVER ! », alors il faut appuyer sur enter pour recommencer.

> Les commandes sont : la flèche droite pour se déplacer vers la droite, la flèche gauche pour se déplacer vers la gauche et la flèche vers le haut pour sauter. C'est si simple !

(for more info, see https://itch.io/jam/im-hungry / https://pancelor.itch.io/im-hungry)

P#110904 2022-04-26 21:12 ( Edited 2022-06-01 14:24)

[ :: Read More :: ]

I hit this bug while working on a tweetcart:

?"\*6a"  -- prints 6 'a's    (expected)
?"\*6\"" -- prints 6 quotes  (expected)
?"\*6\n" -- prints 1 newline (unexpected!)
?"\n"    -- prints 1 newline (expected)
P#109613 2022-04-03 02:06

[ :: Read More :: ]

here's a demo cart showing off some different ways to handle input in grid-based games:

Cart #hojohiyomu-1 | 2022-02-24 | Code ▽ | Embed ▽ | License: CC4-BY-NC-SA
27

controls:

  • move around with the arrow keys
  • change "chapters" in the pause menu (enter + arrow keys)
  • slow down the game speed (in the later chapters) in the pause menu

I made this cart as a companion to a blog post about input buffering

P#107587 2022-02-24 09:19 ( Edited 2022-02-24 09:55)

[ :: Read More :: ]

@zep o/

A weird bug has been messing with me recently: pico-8 keeps telling me I have "unsaved changes" when I'm pretty sure I don't. I caught the bug on camera this time:

I don't know how to reproduce it; I tried adding a new tab and messing with the text cursor position (since I wondered if this had something to do with the fix for https://www.lexaloffle.com/bbs/?tid=39379 ) and soon after I was able to trigger the bug. But I was able to trigger the bug without even opening up the code editor -- all I did was load cart1 load cart2 load cart1 over and over again until it said "unsaved changes". strange

Jump to 1:12 and 1:24 in the video to see me trigger the bug two separate times. (the video description has a few other timestamps too)


I'm worried I'll stop trusting that message and accidentally lose real changes!

I think this bug is new as of 0.2.4b; I don't remember it happening beforehand.

P#107008 2022-02-16 05:04 ( Edited 2022-02-16 05:09)

[ :: Read More :: ]

Cart #lasal-1 | 2022-01-19 | Code ▽ | Embed ▽ | No License
15

A short celeste map mod. It's built as a bit of a puzzle, meant to teach you one specific thing about the game's mechanics.

This doesn't require any advanced speedrunning tech (spike clips, corner jumps, etc) -- executing the intended solutions should be possible for anyone who's beaten celeste classic once or twice.

If you're trying something that seems too hard or only barely possible, try looking for alternatives!

controls

  • arrow keys / Z / X: move / jump / dash
  • E: toggle screenshake

credits

celeste classic: maddy thorson + noel berry

smalleste: a token-optimized version of classic celeste that I used as a starting point

playtesting: cryss, sharkwithlasers, James, meep

berry follow code: meep's Terra Australis

map editor

I made this map to see how my custom map editor felt to use. I think it's pretty neat -- check it out! It lets you build much larger maps than the built-in pico-8 map editor.

P#105290 2022-01-19 00:23 ( Edited 2022-01-19 06:31)

[ :: Read More :: ]

demo cart

This demo shows off the interface. To use bigmap in your game, see "setup" below.

Cart #bigmap_demo-1 | 2022-01-18 | Code ▽ | Embed ▽ | No License
41

motivation

The recent 0.2.4 release added support for larger maps:

> Similar to gfx memory mapping, the map can now be placed at address 0x8000 and above (in increments of 0x100). This gives 4 times as much runtime space as the default map, and an additional POKE is provided to allow customisable map sizes.

Larger maps are now possible, but it's difficult to get them into memory -- the built-in map editor only works with vanilla-sized maps.

This cart is a full map editor that makes it easy to make these larger maps!

features

  • tight iteration loop - play a level in your game, jump into the map editor to make a small tweak, and return to the game with minimal friction
  • change map size at any time
    • max width: 256 tiles
    • max height: none
    • max total size: 32K tiles (e.g. 128*256, or 32*1024)
  • easy copy-paste (right mouse + drag to copy, left mouse to paste)
  • zooming in/out
  • large brushes - place multiple tiles at a time
  • show 16x16 "room" outlines (useful for carts like celeste that are made of many 16x16 rooms)
  • "transparency" - optionally treat sprite 0 in large brushes as "transparent"
  • compressed maps using PX9
  • autosaving
  • the map editor uses 0 of your tokens -- it's a completely separate cart that you only use during development
    (well, it costs ~300 tokens to load the map string and run the decompressor)
  • your game will still be splore-compatible
  • your game can call map(), mget(), tline() etc without any extra work

undo/redo?

The editor currently has no undo/redo functionality. That's not ideal! I'm hoping to get it working soon.

To undo all changes since your last save (probably the end of your last session using bigmap), use the "discard changes" button in the top-right.

If you make a large mistake, replace map.p8l with your an autosaved version (inside mygame/autosave/) and reload bigmap.

setup

Setting the bigmap editor up takes a bit of work. This is mainly necessary to enable a tight map iteration loop, and also to work around the restrictions of pico-8 (for example, it's impossible to read a file from disk without user interaction, and asking the user to drag-and-drop their map file every time they wanted to edit is way too much friction for my tastes)

To start, make sure you have a folder (e.g. mygame/) with your game cart inside (e.g. mygame/mygame.p8)

  1. cd mygame (navigate into the folder containing your game)
  2. mkdir autosave (create a folder to store backups/autosaves)
  3. printh("","map.p8l") (this creates an empty map.p8l file)
  4. Save this file (https://gist.github.com/pancelor/f933286f244c6b85b7720dbe6f809143) as px9_decomp.lua (inside the mygame/ directory)
  5. load #bigmap (note: this is different from the bigmap_demo cart)
  6. Uncomment the two #include lines in the second tab (tab 1)
  7. save bigmap.p8
  8. Paste this snippet into mygame.p8: (inside _init(), or at top-level; either works)

    menuitem(1,"โ–’ edit map",function()
      -- pass spritesheet through upper memory,
      -- avoiding an extra second of load time
      local focusx,focusy=0,0
      memcpy(0x8000,0x0000,0x2000)
      poke(0x5500,1,focusx,focusy) --signal
    
      load("bigmap.p8","discard changes","mygame.p8")
    end)

    (make sure you change "mygame.p8" to the actual filename of your game)

    This snippet adds the menu option to enter bigmap while playing your game. If you set focusx and focusy, bigmap will start focused on that map coordinate.

  9. Paste this snippet into mygame.p8 at top-level:
    #include map.p8l
    #include px9_decomp.lua
    if map_import then map_import() end
  10. Save mygame.p8

You should now be good to go!

test+edit iteration loop

  1. Save mygame.p8. any unsaved changes will be lost every time you launch bigmap. (I wish this was avoidable but I couldn't find a way around it that preserved the quick test+edit loop I wanted)
  2. Run mygame.p8
  3. Pause the game (with P or Enter)
  4. Choose "edit map"
  5. Edit your map!
    If you accidentally press escape and exit the map editor, type r or resume into the console to resume the map editor.
  6. Return to your game with P, Enter, or the clickable "Play" button in the top-right

I advise setting up mygame.p8 to jump you to the room you were editing when it starts - this can be done by reading the focusx and focusy global variables that are set inside the map_import() function (inside map.p8l)

See my celeste mod or the getting started video for an example of how to do this.

technical details

When you press the save or play button, bigmap uses printh to write a text file called map.p8l into the current directory. This text file happens to be valid lua code that defines a function called map_import(), so when mygame.p8 executes #include map.p8l and map_import(), it runs the code generated by bigmap.

Here's an example of what map.p8l looks like:

-- this file was auto-generated by bigmap.p8
function map_import()
 focusx=11
 focusy=12
 mapw=128
 maph=64
 poke(0x5f56,0x80,mapw)
 local function vget(x,y) return @(0x8000+x+y*mapw) end
 local function vset(x,y,v) return poke(0x8000+x+y*mapw,v) end
 px9_sdecomp(0,0,vget,vset,"โ—โ—โ—ใƒฆโ—7ใชโ—โ—โœฝ,ใ‚ƒf\0โ˜…)&ใ‚‹ใกP;](♥KใญF ... many many more chars here ... Xโ–คใƒŸใƒ˜ใƒฉโฌ‡๏ธโฌ‡๏ธBใ‚Œใ‚“")
end

This has 3 main parts:

  1. Set up some helpful global vars - mapw and maph are the width and height of the map, in tiles. focusx and focusy are the tile coordinate of the tile that was in the center of the screen when map.p8l was saved -- you can use this info to jump directly to that room to let you make small tweaks and test them very quickly
  2. poke(0x5f56,0x80,mapw) -- this tells pico-8 to use mapdata stored at 0x8000, with map width mapw
  3. The rest of the function uses PX9 to decompress the binary data stored in that long string. The decompressed data gets stored starting at 0x8000 and takes up mapw*maph bytes.

That compressed data string is created using PX9 and this snippet for encoding binary data in strings.

links

stuff I used:

alternative map editors you might consider using instead:

other:

happy map editing!

I'd like to see what you make -- let me know if you use this in your projects! And if you want to credit me I'd appreciate it :)

Is bigmap helpful? Is it too confusing to set up? Find any bugs? Let me know what you think!

P#105301 2022-01-19 00:12 ( Edited 2023-10-06 06:03)

[ :: Read More :: ]

tl;dr

In the pico8 console, run load #prof, then edit the last tab with some code you want to measure:

prof(function(x)
  local _=sqrt(x)   -- code to measure
end,function(x)
  local _=x^0.5     -- some other code to measure
end,{ locals={9} }) -- "locals" (optional) are passed in as args

Run the cart: it will tell you exactly how many cycles it takes to run each code snippet.


what is this?

The wiki is helpful to look up CPU costs for various bits of code, but I often prefer to directly compare two larger snippets of code against each other. (plus, the wiki can get out of date sometimes)

For the curious, here's how I'm able to calculate exact cycle counts
(essentially, I run the code many times and compare it against running nothing many times, using stat(1) and stat(2) for timing)

-- slightly simplified from the version in the cart
function profile_one(func)
  local n = 0x1000

  -- we want to type
  --   local m = 0x80_0000/n
  -- but 8๐˜ฎ๐˜ฉz is too large a number to handle in pico-8,
  -- so we do (0x80_0000>>16)/(n>>16) instead
  -- (n is always an integer, so n>>16 won't lose any bits)
  local m = 0x80/(n>>16)

  -- given three timestamps (pre-calibration, middle, post-measurement),
  --   calculate how many more ๐˜ค๐˜ฑ๐˜ถ cycles func() took compared to noop()
  -- derivation:
  --   ๐˜ต := ((t2-t1)-(t1-t0))/n (frames)
  --     this is the extra time for each func call, compared to noop
  --     this is measured in #-of-frames (at 30fps) -- it will be a small fraction for most ops
  --   ๐˜ง := 1/30 (seconds/frame)
  --     this is just the framerate that the tests run at, not the framerate of your game
  --     can get this programmatically with stat(8) if you really wanted to
  --   ๐˜ฎ := 256*256*128 = 8๐˜ฎ๐˜ฉz (cycles/second)
  --     (๐˜ฑ๐˜ช๐˜ค๐˜ฐ-8 runs at 8๐˜ฎ๐˜ฉz; see https://www.lexaloffle.com/dl/docs/pico-8_manual.html#CPU)
  --   cycles := ๐˜ต frames * ๐˜ง seconds/frame * ๐˜ฎ cycles/second
  -- optimization / working around pico-8's fixed point numbers:
  --   ๐˜ต2 := ๐˜ต*n = (t2-t1)-(t1-t0)
  --   ๐˜ฎ2 := ๐˜ฎ/n := m (e.g. when n is 0x1000, m is 0x800)
  --   cycles := ๐˜ต2*๐˜ฎ2*๐˜ง
  local function cycles(t0,t1,t2) return ((t2-t1)-(t1-t0))*m/30 end

  local noop=function() end -- this must be local, because func is local
  flip()
  local atot,asys=stat(1),stat(2)
  for i=1,n do noop() end -- calibrate
  local btot,bsys=stat(1),stat(2)
  for i=1,n do func() end -- measure
  local ctot,csys=stat(1),stat(2)

  -- gather results
  local tot=cycles(atot,btot,ctot)
  local sys=cycles(asys,bsys,csys)
  return {
    lua=tot-sys,
    sys=sys,
    total=tot,
  }
end

how do I use it?

Here's an older demo to wow you:

Cart #cyclecounter-2 | 2022-01-16 | Code ▽ | Embed ▽ | License: CC4-BY-NC-SA
15

This is neat but impractical; for everyday usage, you'll want to load #prof and edit the last tab.

The cart comes with detailed instructions, reproduced here for your convenience:

-----------------------
-- โ˜… usage guide โ˜… --
-----------------------

์›ƒ: i have two code snippets;
    which one is faster?

๐Ÿฑ: edit the last tab with your
    snippets, then run the cart.
    it will tell you precisely
    how much cpu it takes to
    run each snippet.

    the results are also copied
    to your clipboard.

์›ƒ: what do the numbers mean?

๐Ÿฑ: the cpu cost is reported
    as lua and system cycle
    counts. look up stat(1)
    and stat(2) for more info.

    if you're not sure, just
    look at the first number.
    lower is faster (better)

์›ƒ: why "{locals={9}}"
    in the example?

๐Ÿฑ: accessing local variables
    is faster than global vars.

    so if your test involves
    local variables, simulate
    this by passing them in:

      prof(function(a)
        sqrt(a)
      end,{ locals={9} })

    /!\     /!\     /!\     /!\
    local values from outside
    the current scope are also
    slower to access! example:

      global = 4
      local outer = 4
      prof(function(x)
        local _ = x --fast
      end,function(x)
        local _ = outer --slow
      end,function(x)
        local _ = global --slow
      end,{ locals={4} })
    /!\     /!\     /!\     /!\

์›ƒ: can i do "prof(myfunc)"?

๐Ÿฑ: no, this sometimes gives
    wrong results! always use
    inline functions:

      prof(function()
        --code for myfunc here
      end)

    as an example, "prof(sin)"
    reports "-2" -- wrong! but
    "prof(function()sin()end)"
    correctly reports "4"

    (see the technical notes at
    the start of the next tab
    for a brief explanation.
    technically, "prof(myfunc)"
    will work if myfunc was made
    by the user, but you will
    risk confusing yourself)

There are also instructions included on two alternate ways you can profile your code, without using prof:

---------------
 โ˜… method 2 โ˜…
---------------

this cart is based on
code by samhocevar:
https://www.lexaloffle.com/bbs/?pid=60198#p

if you do this method, be very
careful with local/global vars.
it's very easy to accidentally
measure the wrong thing.

here's an example of how to
measure cycles (ignoring this
cart and using the old method)

  function _init()
    local a=11.2 -- locals

    local n=1024
    flip()
    local tot1,sys1=stat(1),stat(2)
    for i=1,n do   end --calibrate
    local tot2,sys2=stat(1),stat(2)
    for i=1,n do local _=sqrt(a) end --measure
    local tot3,sys3=stat(1),stat(2)

    function cyc(t0,t1,t2) return ((t2-t1)-(t1-t0))*128/n*256/stat(8)*256 end
    local lua = cyc(tot1-sys1,tot2-sys2,tot3-sys3)
    local sys = cyc(sys1,sys2,sys3)
    print(lua.."+"..sys.."="..(lua+sys).." (lua+sys)")
  end

run this once, see the results,
then change the "measure" line
to some other code you want
to measure.

note: wrapping the code inside
"_init()" is required, otherwise
builtin functions like "sin"
will be measured wrong.
(the reason is explained at
the start of the next tab)

---------------
 โ˜… method 3 โ˜…
---------------

another way to measure cpu cost
is to run something like this:

  function _draw()
    cls(1)
    local x=9
    for i=1,1000 do
      local a=sqrt(x) --snippet1
  --    local b=x^0.5 --snippet2
    end
  end

while running, press ctrl-p to
see the performance monitor.
the middle number shows how much
of cpu is being used, as a
fraction. (0.60 = 60% used)

now, change the comments on the
two code snippets inside _draw()
and re-run. compare the new
result with the old to determine
which snippet is faster.

note: every loop iteration costs
an additional 2 cycles, so the
ratio of the two fractions will
not match the ratio of the 
execution time of the snippets.
but this method can quickly tell
you which snippet is faster.

various results

Here are some speed comparisons I found interesting. Some of these may be out of date now, but they were interesting:

poke4 v. memcopy

prof(function() memcpy(0,0x200,64) end,       -- 71 (7 lua, 64 sys)
     function() poke4(0,peek4(0x200,16)) end) -- 67 (7 lua, 60 sys)

Copying 64 bytes of memory is very slightly faster if you use poke4 instead of memcpy -- interesting!
(iirc this is true for other data sizes... find out for yourself for sure by downloading and running the cart!)

edit: this has changed in 0.2.4b! the memcpy in this example now takes 39 cycles

constant folding

I thought lua code was not optimized by the lua compiler/JIT at all, but it turns out there are a few specific optimizations it will do.

prof(function() return 2+2 end,
     function() return 2+2+2+2+2+2+2+2 end)

These functions both take a single cycle! That long addition gets optimized by lua, apparently. @luchak found these explanations:

https://stackoverflow.com/questions/33991369/does-the-lua-compiler-optimize-local-vars/33995520
> Since Lua often compiles source code into byte code on the fly, it is designed to be a fast single-pass compiler. It does do some constant folding

A No Frills Introduction to Lua 5.1 VM Instructions (book)
> As of Lua 5.1, the parser and code generator can perform limited constant expression folding or evaluation. Constant folding only works for binary arithmetic operators and the unary minus operator (UNM, which will be covered next.) There is no equivalent optimization for relational, boolean or string operators.

constant folding...?

One further test case:

prof(function() local a=2  return 2+2+2+2+2+2+2+a end, --2
     function() local a=2  return a+2+2+2+2+2+2+2 end) --8

These cost different amounts! Constant-folding only seems to work at the start of expressions. (This is all highly impractical code anyway, but it's fun to dig in and figure out this sort of thing)

credits

Cart by pancelor.

Thanks to @samhocevar for the initial snippet that I used as a basis for this profiler!

Thanks to @freds72 and @luchak for discussing an earlier version of this with me!

Thanks to thisismypassword for updating the wiki's CPU page!

changelog

v1.4

  • redo explanations
  • more thorough explanation of pitfalls of alternate methods
    • why measuring sin() at top-level is no good
    • why function _draw() for i=1,1000 do ... end end can be misleading

v1.3

  • simpler BBS post, friendlier cart instructions

v1.2

  • rewrite; recommend using load #prof instead now

v1.1

  • added: press X to copy to clipboard
  • added: can pass args; e.g. profile("lerp", lerp, {args={1,4,0.3}})

v1.0

  • intial release
P#104795 2022-01-11 03:31 ( Edited 2024-03-11 09:38)

[ :: Read More :: ]

Cart #linecook-3 | 2023-10-30 | Code ▽ | Embed ▽ | No License
4

these busy birds will eat almost anything that falls into their gullet -- what will you feed them? they have their preferences, but people food beats bird food any day of the week!

a difficult, chaotic arcade game. now with local multiplayer support! also available on itch.

controls:

  • left / right: move
  • x / up: grab
  • ESDF: movement keys for player 2

features

  • 4 difficulty modes: "easy", medium, hard, and practice
  • 3 different control schemes:
    • solo
    • local multiplayer
    • two-handed singleplayer
  • 4 challenging maps for 4 different flavors of gameplay

Recommended play order

The game modes combine into a whole mess of different options; here's a recommended playthrough order:

  1. Play through practice mode
  2. Check out the recipe book if you want to see the recipes
  3. Try COOKS:1 MAP:NEST LEVEL:EASY
  4. Try COOKS:2 MAP:NEST LEVEL:EASY -- play with one hand on the arrow keys and one hand on ESDF
  5. Try each of the 4 maps with whichever COOKS and LEVEL settings you prefer
  6. Try to beat MAP:CANON LEVEL:MEDIUM !

#ChainLetterJam

this was made for the #ChainLetterJam! Patrick nominated me; I had fun playing Arithmetic Bounce (my high score on hard mode is 24), so I decided to try making a game that would feel similarly chaotic.

I took inspiration from the fact that none of the target numbers in Arithmetic Bounce were inherently good or bad; their value changed depending on the current goal. This became the ingredient/recipe idea in linecook: specific ingredients are sometimes good and sometimes bad, depending on the current recipe. I also liked the chaos and time pressure caused by gravity in Arithmetic Bounce; I've gone for a slightly different but still chaotic spin by giving the player two grabber-claws that are tricky to aim.

the continuation of this chain is: https://tallywinkle.itch.io/the-witchs-almanac

blog

I wrote about this game's design here. tl;dr: if you allow your game systems to play out as physical processes in the game world, there's more opportunity for surprising interactions

hope you enjoy it!

P#103294 2021-12-22 02:03 ( Edited 2023-10-30 08:50)

[ :: Read More :: ]

0.2.4 has been released, and we now have an extra segment of memory to play with from 0x8000 to 0xffff. That's 32K, or 0x8000 bytes. A spritesheet takes up 0x2000 bytes... so we could stuff 4 extra spritesheets in there!

I've created a system where you can call my custom function cspr the same way you would normally call spr, and everything "just works". The difference is, cspr can handle up to 1024 sprites instead of the standard 256-sprite-limit of spr. (also, cspr is a bit slower (but not much!) than spr, because it has to manage a cache)

Here's a demo that uses 4 full spritesheets; search the code for "cspr" to see how easy it is to use, once you've set it up!

Cart #hefafanino-4 | 2021-12-23 | Code ▽ | Embed ▽ | License: CC4-BY-NC-SA
25

(the games are all by me: #linecook, #remains, #ocelotsafari, and #escalatorworld)

technical details

The core ideas are the blit and cspr functions:

  • blit: this is used to move sprites between the 4 upper spritesheets and the sprite cache (located at 0x0000, where the spritesheet normally is)
  • cspr: cspr_simple gets the core idea of the sprite caching across; the full cspr in the cart can deal with any arguments you would normally pass to spr (like the width/height/flip parameters)

These are the simple/readable versions; the versions in the cart have some optimizations such as replacing y0*64 with y0<<6 that may make it more difficult to follow:

-- copies a sprite from x0,y0
--   (from spritesheet based at
--   memory address base0)
--   into x1,y1 on the spritesheet
--   based at address base1
-- set base1 to 0x6000 to use the
--   screen as the destination
-- all coordinates are measured
--   in pixels
-- note! odd x-coordinates will
--   be rounded down
-- by pancelor
function blit(base1,x1,y1,base0,x0,y0, w,h)
 local a0=base0+y0*64+x0\2 --source
 local a1=base1+y1*64+x1\2 --destination
 local w2=w and w\2 or 4   --half-width
 for da=0,(h or 8)*64-1,64 do
  memcpy(a1+da,a0+da,w2)
 end
end

-- "cached sprite" by pancelor
--  uses a direct-mapped cache
--  up to 4 spritesheets are
--   stored in 0x8000+
--  they are numbered 0-1023
--  using any sprite s will write
--   it to spritesheet slot s%256
--   and then use it from there
_cspr_bank={} -- maps slots (0-255) to which bank the sprite comes from (0-3)
function cspr_simple(sbig,x,y)
 local bank,s=sbig>>8&3,sbig&0xff
 -- bank is 0-3, sbig is 0-255 (inclusive)
 if _cspr_bank[s]~=bank then
  -- cache miss!
  -- blit sprite s from its bank into the cache:
  local sx,sy=s%16*8,s\16*8
  blit(0,sx,sy,
       0x8000+bank*0x2000,sx,sy,
       8,8)
  _cspr_bank[s]=bank
 end
 spr(s,x,y)
end

drawbacks:

An earlier version of this demo used something called upspr instead of cspr and had many drawbacks (do load #hefafanino-1 in your local pico-8 to see that old version). I've updated the demo cart to a way better version that "just works", using cspr.

  1. sprites can only be blitted on even x-pixel values. e.g., upspr(290,11,11) will draw sprite 290 to 10,11 instead
  2. you can't store anything inside upper memory until runtime, so you'll probably want to use PX9 or something similar to store your extra spritesheets (as code strings? inside 0x0000-0x2000?) and then decompress them at startup
  3. flipping sprites takes extra work
  4. palette and transparency will not be respected
  5. sprite editing is more difficult (due to 2)
  6. the upper memory is being used by spritesheets, which may get in the way of other uses for the upper memory (such as larger maps). But you don't need to completely fill the upper memory with spritesheets; you could have, say, 512 sprites (2x normal) and still have 0x4000 bytes leftover for a 128x128 map. we've got more room for either, but we still have to make a tradeoff between the two!
  7. map() no longer works -- you'll need to write your own version of map() that calls spr() repeatedly. luckily this is about as fast as calling map() directly. note that mget() and mset() will need to be rewritten too, because they only handle 1-byte entries.

extension ideas:

  1. if you stored all of your sprites in upper memory and used the spritesheet at 0x0000 as a cache for the sprites you're actively using, you can fix drawbacks 1, 3, and 4 (above) "for free". See "Virtual Sprites" in @freds72's POOM devlog for an idea of how to do this
    • Done! I used a direct-mapping cache instead of an LRU cache, which might cause performance problems if you repeatedly draw sprite x and then sprite x+256*n (because those sprites both map to the same slot, x). For example, sprites 17, 256+17,512+17, and 768+17 all get stored in slot 17 of my direct-mapped sprite cache.
    • However, cspr/blit is quite fast, so you might not even see performance problems. (see my next post for performance details)

recommendations:

Someone on discord asked: "should people draw all sprites from bank 1, then swap for bank 2, etc?"

My recommendation: That would help, but I don't recommend it -- there are more effective ways to improve the performance, I think:

  • If you're okay with needing to manage which sprite banks are currently loaded, you could avoid all of the overhead of calling cspr() and just call spr() directly. This lets you use map(), too. You would of course need to manually memcpy the sprite banks into the 0x0000 region when appropriate.
  • If you want better performance but want a low-maintenance cache that's easy to use, you should probably write an LRU cache instead of my simple direct-mapped cache. (I may do this myself soon)

is cspr good enough to just use?

I think so! it's fast enough; it uses up to 10~15% of a frame (in my limited testing) and you get 4x the sprite space, without needing to think about the cache at all. An LRU cache might make this number way better, but I haven't tried that yet.

Keep in mind that map() does not work with cspr -- this may be a dealbreaker for some. (you'll need to roll your own implementation of map/mget/mset)

If you are willing to give up the "without needing to think about the cache at all" requirement, you should maybe manually move pages of sprites (128x32? 128x64? 128x128?) around instead -- it'd be mostly pretty simple, and very token-efficient. (thanks to merwok for the suggestion!)

P#103168 2021-12-20 12:38 ( Edited 2021-12-23 08:37)

[ :: Read More :: ]

I've hit a really bizarre error; my code has no coroutines, but it fails with "attempt to yield from outside a coroutine"

I've simplified it down as much as I could; while removing unrelated cruft (e.g. the sort function from my new project template that I wasn't actually using anywhere) the bug would arbitrarily appear or disappear.

The bug will either get triggered 100% of the time or 0% of the time when you run the cart, but its presence or disappearance arbitrarily changes depending on what other unrelated code there is in the cart.

For example, tab 0 is 15K of function foo() end repeated over and over again. They're commented out right now, and the bug is present. If you comment them back in, the bug disappears.

Commenting out the body of pqb (which prints many many u64 objects) seems to remove the bug for good (no matter how many foos I comment in or out). I'm not too experienced with metatables; I think I may be at fault for something I'm doing inside u64.__tostring? sometime variation of this code give a slightly differently-formatted error message that jumps me into the middle of my __tostring method. or maybe printh is having issues dumping so much text to the console? I dunno

I'm using pico-8 0.2.4 for windows 7.

Cart #pbomdme-0 | 2021-12-12 | Code ▽ | Embed ▽ | License: CC4-BY-NC-SA


the more common error screen:


the more rare error screen (can be triggered if there are exactly 144 foos in tab 0)

P#102444 2021-12-12 04:06

[ :: Read More :: ]

This is #bubblecat-0

  • It doesn't reload properly (launch it and then press ctrl-R; an "attempt to call a string value" error message appears, and is very different from the normal error messages. pico-8 is unresponsive after this)
  • It does reload fine through the menu (press P; choose "reset cart")

Cart #bubblecat-0 | 2021-11-20 | Code ▽ | Embed ▽ | No License
3


This is #bubblecat-2

  • It reloads just fine (using both methods)
  • However, reloading with ctrl-R causes a strange "loaded external changes" message

Cart #bubblecat-2 | 2021-11-20 | Code ▽ | Embed ▽ | No License
3


The difference between the two carts is a single newline between the end of the __lua__ section and the __gfx__ section:

These errors only happen in the web player or exported binaries; they do not happen inside pico8.exe.

I'm 90% sure I created #bubblecat inside pico8 itself; I didn't edit bubblecat.p8 using my external text editor.


Expected behavior:

  • a cart without a final newline in the last tab should work fine in the web player and in binary exports
  • no "loaded external changes" message should be shown when reloading

I'm running pico8 0.2.4; I noticed this issue on 0.2.3 but I believe the web player is running 0.2.4, and I confirmed the issue is present in binary exports for windows from 0.2.4

P#101396 2021-12-04 01:10 ( Edited 2021-12-04 01:18)

[ :: Read More :: ]

Cart #bubblecat-2 | 2021-11-20 | Code ▽ | Embed ▽ | No License
3

welcome back, bubble cat. we have another situation, and this time you've only got 60 seconds

how to play:

  • arrow keys: move
  • ctrl-m: mute
  • X/Z: continue to next level

you don't have to full-clear every level! skipping a level without clearing it just gives you a points penalty (-5 per remaining bubble)

code:

this game was made to fit inside two tweets; i.e. <560 characters of code and no sprites! here's the full code:

x=3y=3o={}m=0n=0p=circfill::_::z={}for j=1,13do
z[j]=rnd(49)\1end?"โถ!5f2cC"
while t()<60do?"โถ1โถc"
b=btnp()q=b>8and"โทfdc"d=sgn(n-m)u=x
if(n~=m)m+=d d+=6
p(32,32,60-t(),1)v=y?"โถwโถt"..m,24,27,4+d
if(b%16>0)s=b*.5938&.75
if(not r)r,s=s
if(r)x+=cos(r)y+=sin(r)
w=x\7+y\7~=0
for j=#z,1,-1do
i=z[j]d=q and"-5"b=i\7*9a=i%7*9p(a+4,b+4,3,j|8)
if(y*7+x-x\7==i)w=del(z,i)d="+1"?"โทd"
if(#z<1)d="+10"q="โทegc4"
if(d)add(o,{d,a,b,d*11})n+=d
end?"โ˜…โต8d๐Ÿฑ",x*9+1,y*9+3,7
if(w)x,y,r=u,v
for a in all(o)do
a[3]-=1?unpack(a)
end?q or""
if(q)goto _
end?"โทdafa"
::e::goto e

(that's only 548 characters, but some of them (like the cat face) cost two characters on twitter. remember to ctrl-p to enter puny-text mode before pasting this into your local console!)

some code highlights:

  • convert btnp() bitfield into movement: b=btnp()if(b>0)s=b*.5938&.75if(r)x+=cos(r)y+=sin(r)
  • buffered input: if(not r)r,s=s
  • out-of-bounds check: w=x\7+y\7~=0
  • animated score display: d=sgn(n-m) if(n~=m)m+=d
  • collision checking: if(y*7+x-x\7==i)
  • draw player: ?"โ˜…โต8d๐Ÿฑ",x*9+1,y*9+3,7
  • animated score floaters: for a in all(o)do a[3]-=1?unpack(a) end

full code history:

https://github.com/pancelor/bubble-cat

high score:

my high score is 207! 240! what's yours?

P#100464 2021-11-20 03:23 ( Edited 2022-01-02 20:31)

[ :: Read More :: ]

I was messing around with the new p8scii memset command, and it sometimes crashes my console at weird times. once, it crashed when I did this:

?"\^!0000"

(i.e. using memset with an address but no actual arguments)

Another time I did a similar command and then did "reboot", and it crashed then


system info: pico-8 0.2.3 / windows 7

the error message:
> Microsoft Visual C++ Runtime Library
>
> This application has requested the Runtime to terminate it in an unusual way.
> Please contact the application's support team for more information.

P#99291 2021-10-28 23:27

[ :: Read More :: ]

Cart #firroref-0 | 2021-10-15 | Code ▽ | Embed ▽ | License: CC4-BY-NC-SA

Here's some code:

poke(0x5f5e,0x11) --only enable bitplane 1
sset(0,0,15) --edit sprite 0

I would expect the sset() call to set the spritesheet's corner to color 1 (dark blue) because of the bitplane setting. However, this instead sets the corner to 15 (tan).

I can workaround this for now by using pset and then memcopying the screen to the spritesheet, but that means my decompression code (which wants to call sset with bitplanes active) will need to either take less than a frame to run, or show artifacts onscreen while it runs.

P#98668 2021-10-15 01:19

[ :: Read More :: ]

I just found out that you can turn a specific sprite into the icon for the binary export: https://www.lexaloffle.com/dl/docs/pico-8_manual.html#Binary_Applications_ and whoa, this is really great! But afaict it's restricted to the standard palette. Is there some way to set a custom palette for the icon in the export? If not, adding some sort of palette flag might be a nice feature:

EXPORT -I 32 -C 12 -Z 0,132,4,140,134,6,135,7,8,137,139,11,138,130,13,131 MYGAME.BIN

or maybe reuse the -C flag, and a -1 entry means transparent?

EXPORT -I 32 -C 0,132,4,140,134,6,135,7,8,137,139,11,-1,130,13,131 MYGAME.BIN

(although that might be a bit awkward because using pal(12,-1,1) ingame means to map 12->0x8F, not 12->transparent...)

P#98352 2021-10-07 20:06

[ :: Read More :: ]

Cart #freecell1k-0 | 2021-09-28 | Code ▽ | Embed ▽ | License: CC4-BY-NC-SA
8

Free Cell, in 1022 characters of code, and no sprites! twitter | itch

CONTROLS:

  • click and drag cards around
    • you cannot move stacks of cards; one at a time only!
  • stack descending cards of alternating colors in the main play area
    • any card may be placed in an empty column of the main area
  • store any card in the four "free cells" at the top left
  • make a stack of each suit A->K in the top right to win
  • reset the cart to start a new game

ONE AT A TIME?

You can only move one card at a time; if you want to move a stack of cards you have to take it apart and put it back together manually. This is different from "standard" solitaire, and it makes Free Cell particularly interesting! It also makes the implementation a bit easier to fit into the tiny code-size constraint ;)

I WON!

Congrats! Enjoy the win animation here: https://pancelor.itch.io/solitaire-win-animation (I wanted to add a "you win" animation to this game, but I didn't have the room to fit it in... so I made it as a separate cartridge)

SOURCE CODE

The source is in the cart, of course, but it's here too. Remember to enable puny text mode (cmd-p) before pasting this into your local PICO-8 console:

A=add
W=12w=13Z=16s={}B={}Q=1T=0M=9q=poke2
function F(i)S=s[i]J=i\W I=i\8O=1-I U=I*(i-8+J)*14+O*i*Z+2V=O*max(#S*6+22,28)+5end
for i=0,51do s[i]={m=i\W}A(B,{x=i,y=400,k=i+i\w*3},rnd(i+1)+1)end
q(-15-๐Ÿ˜,264,2043,4,3843)D=rectfill::_::L=T%8T+=1K=B[T]N=not btn(5)X=stat(32)-6Y=stat(33)-8C=fillp
if(T>52)q(14-๐Ÿ˜,3)M=T%3
for i=15,0,-1do
F(i)M|=S.m
C(โ–’)a=5+O*28D(U,a,U+W,a+Z,2)C()for _ENV in all(S)do x=u+3*x+.5>>2y=v+3*y+.5>>2end
G=abs(X-U)\8+abs(Y-V)\Z<<6v=S[#S]k=w
if(v)k=v.k
if(2>>k%Z>M+J*2)K=A(B,del(B,v))L=W+k\Z
if(H and N and(J+G+#S<1or Z-I|k+G==Z|1+H.k^^32or H.k==J+k|G))K=H L=i
if(btnp(5+J+G))H=A(B,del(B,v))end
if(K)del(s[K.h],A(s[L],K))F(L)S.m/=2K.u,K.v,K.h=U,V,L?"โทi6v1d1"
if(H)H.x,H.y=X,Y
if(N)H=nil
for r in all(B)do
x=r.x
y=r.y
q(63-๐Ÿ˜,244)D(x-1,y-1,x+W,y+Z,4)q(61-๐Ÿ˜,-1,-1)D(x,y,x+W,y+Z,3)a=r.k%Z+1?(a==10and"³f|³f0 ³b"or sub("a23456789|jqk",a,a).." ³d")..split("♥,โ—†,โ—†โต8f..³aแถœ3.,โ—†โต8fใƒ‹")[r.k\Z+1],x+1,y+1,r.k\32
for i=0,77do
pset(x+W-i%w,y+Z-i\w,pget(x+i%w,y+i\w))end
end?"โ—โถ1โถc6",X+2,Y+7,5
if(M< Q/โง—)Q=0?"โทceg4"
goto _

(there's an extra space in there near the end because the BBS text editor seems to choke on the < symbol)

An earlier, more readable, and much longer version of this code can be found here

SOME CODE HIGHLIGHTS

Some of the more bizarre tricks I used to squeeze every bit of functionality out of my 1024-character budget:

  • set the palette with poke2(-15-๐Ÿ˜,264,2043,4,3843)
  • update card positions with for _ENV in all(S)do x=u+3*x+.5>>2y=v+3*y+.5>>2end
  • dynamically cast shadows with a very particular palette and poke2(63-๐Ÿ˜,244)rectfill(x-1,y-1,x+W,y+Z,4)poke2(61-๐Ÿ˜,-1,-1)
  • draw the 4 suit icons with split("♥,โ—†,โ—†โต8f..³aแถœ3.,โ—†โต8fใƒ‹")[suit_id]
  • check if you can drop your held card with i\12+G+#S<1or 16-i\8|k+G==16|1+H.k^^32or H.k==i\12+k|G
  • wait until the next frame and clear the screen with ?"โถ1โถc6"

    • (thanks to zep for pointing out that \^ can be written as โถ!)
  • auto-move cards to the top right by tracking the minimum stack height with bitshifting (search for M (and m) to see the relevant code)
  • check whether the game is won by taking advantage of the fact that 2^12<โง— and โง—<2^13

I'M SORRY, "poke2(63-๐Ÿ˜,244)"???

Yeah! 63-๐Ÿ˜ is 24414.5, which is the address I need to poke to get those slick shadows! Check out this post for more info.

P#97939 2021-09-28 22:10 ( Edited 2021-09-30 04:04)

[ :: Read More :: ]

Cart #constantcompanion-8 | 2022-09-02 | Code ▽ | Embed ▽ | License: CC4-BY-NC-SA
27

controls:

This is a niche tool to help you save characters when writing carts that are codesize-constrained.

  • type in a number; press enter
  • then press up/down and ctrl-c/enter to copy a code
  • navigate back up (or press backspace) to start typing a new number

Example: 0x6000 can be written as 0x6000 (6 chars), 24576 (5 chars), 6^13 (4 chars) or โŒ‚-๐Ÿฑ (3 chars!)

The tool sorts the results by character count (on twitter), so the top results are your best bet.

Update: The latest versions of pico8 have special P8SCII codes for poking which are often fewer characters than calling poke directly (even after using this tool). e.g. ?"\^!5f10249?" instead of poke(0x5f10,50,52,57,63). Consider using that instead! But this tool might still be useful sometimes, and at the very least, it's an interesting artifact.

motivation:

While I was making Free Cell 1K (itch | twitter | bbs) for the #Pico1K jam, I realized I could save characters by taking advantage of the built-in constants. I needed to poke a few things to various addresses (for setting the palette, drawing dynamic shadows with bitplanes, and enabling the mouse) so I had code that looked like this:

-- 94 chars (not including comments or newlines)
poke(0x5F10,8,1,251,7,4,0,3,15) --palette
poke(0x5F2D,3) --mouse
poke(0x5F5C,-1) --disable btnp repeat
poke(0x5F5E,0xF4) --shadows on
poke(0x5F5E,0xFF) --shadows off

Each of those poke addresses uses 6 characters; we can do better! The first thing I did to save characters was this:

-- 84 chars (not including comments or newlines)
A=24365
poke(A-78,8,1,251,7,4,0,3,15)
poke(A-49,3)
poke(A-2,-1)
poke(A,0xF4)
poke(A,0xFF)

But then I saw @zep tweet a trick for writing sqrt(x) as x^โ–ˆ instead (because โ–ˆ (shift+A) is defined to have a value of 0.5), and I realized I could do even better: the ๐Ÿ˜ (shift+M) character is defined to have a value of -24351.5, so I did this:

-- 95 chars (not including comments or newlines)
poke(-15.5-๐Ÿ˜,8,1,251,7,4,0,3,15)
poke(13.5-๐Ÿ˜,3)
poke(60.5-๐Ÿ˜,-1)
poke(62.5-๐Ÿ˜,0xF4)
poke(62.5-๐Ÿ˜,0xFF)

95 chars is not an improvement... yet! However, poke() ignores fractional addresses, so those .5s aren't necessary. Also, I was able to combine the 0x5F5C and 0x5F5E pokes into a single poke, which nullifies some of the relative advantage of the A=24365 technique. In the end, this was the shortest code I could find:

-- 64 chars (not including comments or newlines)
q=poke2
q(-15-๐Ÿ˜,264,2043,4,3843)
q(14-๐Ÿ˜,3)
q(63-๐Ÿ˜,244)
q(61-๐Ÿ˜,-1,-1)

This saves 2 characters over the equivalent version that uses A=24365 instead of the moon face. (or maybe just 1 character, if the newline after q=poke2 can't be removed)

how did you know moon face was the one to use?!

I didn't! I wrote a program to brute-force try all the built-in symbols and see if any were useful for my needs. Check out tab 5 of the cart ("analysis") to see the brute-force algorithm I used.

I've cleaned that program up and posted it here for you. It might be less useful for tweetcarts (because stuff like ๐Ÿ˜ takes up 2 characters on twitter) but it saved me 1~2 entire characters (genuinely very helpful!) during the Pico1K jam, and I hope it helps you too.

Leave a message here or tag me on twitter if you found it useful; I'd like to see what you make!

how does it work?

  1. build a list of all 150 "symbols" under consideration:
    • โ–ˆโ–’๐Ÿฑโ–‘โœฝโ—♥โ˜‰์›ƒโŒ‚๐Ÿ˜โ™ชโ—†…โ˜…โง—ห‡∧โ–คโ–ฅ (most symbols)
    • the numbers 0-9
    • and the negated version of everything above
    • the numbers 10-99 (but not their negated versions)
  2. try combining every pair of symbols a,b with these operations:
    a
    a*b
    a+b
    a&b
    a|b
    a~b
    a-b
    a/b
    a^b
  3. if any result is between target and target+1, display the result. this was chosen because poke/memset/etc round their inputs down to the nearest integer. custom checks are easy to hack in yourself; search for "function near" in the code

changelog

  • v8:
    • add support for new pico-8 syntax a~b for bitwise not (1 char shorter than the old a^^b)
    • sort results by twitter character count instead of naive character count. thanks to jadelombax for the handy reference table here: https://www.lexaloffle.com/bbs/?tid=44375
  • v7: fix typo in v6
  • v6:
    • fix exponentiation parse order (-a^b gets parsed by pico8 as -(a^b), unlike any other operation on the list)
    • increase list scroll speed
    • clean up output list results a bit (e.g. no more -a+b, since b-a is shorter)
    • remove ~x from consideration; it's very similar mathematically to -x, so calculating it was making all searches slower for hardly any benefit. (the code is commented out, so you can re-add it yourself pretty easily if you download the cart and search for ~)
    • make it easier to do custom "is close enough" checking; search for "function near" in the code
  • v5: show options (and allow them to be copied) as soon as they're found. (no longer need to wait for the entire search to complete)
P#97937 2021-09-28 21:48 ( Edited 2022-09-02 22:49)

[ :: Read More :: ]

Cart #ocelotsafari-0 | 2021-04-29 | Code ▽ | Embed ▽ | No License
12

welcome to the ocelot safari!

enjoy the ocelots, and do let us know if you find any long-lost relics deep in the jungle :)

instructions

  • hold Z to drag things
  • arrow keys to move
  • retrieve the lost gemstone of Tezcatlipoca! some say it’s as far as fvkgl-sbhe meters deep in the jungle!

tips

  • we'll leave you some new tools at the initial drop point, if the ocelots steal your gear
  • ocelots can crawl through vines and trees -- they're tricksters!
  • be sure to bring some matches; the nights are long and dark, and who knows what lurks in the jungle...

tutorial

I didn't make the time to make an interactive tutorial, so here's a video instead:

And here's a gif showing how to use each tool:

(light a fire by bumping matches into wood)

good luck in there!


postscript

  • This was made for the Ludum Dare 48 compo in 48 hours. (plus minor bugfixes; read changelog here and here). Rate my entry! https://ldjam.com/events/ludum-dare/48/ocelot-safari
  • My initial goal was to make a game exploring how items feel if you have no inventory system or "use item" button, and I'm happy with the results. Sometimes movement can feel a bit awkward, especially at the start, but that's what the whole game is built around, so I think it's fine.
  • I really like how the nighttime and the ocelots make it a visceral struggle to advance deeper and deeper into the jungle.
P#91318 2021-04-29 22:01

View Older Posts